﻿using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Slock.GameObjects;
using Slock.GameObjects.ActorComponents;
using Microsoft.Xna.Framework.Graphics;

namespace Slock.Components
{
    /// <summary>
    /// A node in an Octal Tree SceneGraph
    /// </summary>
    public class OctTreeNode
    {
        private static int modelsStoredInQuadTree;
        private float size;

        private Vector3 center;
        private BoundingBox boundingBox;

        private List<DrawableActorProxy> drawableActors;
        private List<OctTreeNode> children;

        private OctTreeNode nodeUFL;
        private OctTreeNode nodeUFR;
        private OctTreeNode nodeUBL;
        private OctTreeNode nodeUBR;
        private OctTreeNode nodeDFL;
        private OctTreeNode nodeDFR;
        private OctTreeNode nodeDBL;
        private OctTreeNode nodeDBR;
        private OctTreeNode parent;

        /// <summary>
        /// Initializes a new instance of OctTreeNode
        /// </summary>
        /// <param name="center"></param>
        /// <param name="size"></param>
        public OctTreeNode(OctTreeNode parent, Vector3 center, float size)
        {
            this.parent = parent;
            this.center = center;
            this.size = size;
            this.drawableActors = new List<DrawableActorProxy>();
            this.children = new List<OctTreeNode>(8);

            Vector3 diagonalVector = new Vector3(size / 2.0f);
            this.boundingBox = new BoundingBox(center - diagonalVector, center + diagonalVector);

        }

        /// <summary>
        /// Adds an actor to the tree
        /// </summary>
        /// <param name="actor">The actor to add</param>
        /// <returns>The id of the actor added</returns>
        public int Add(Actor actor)
        {
            RenderComponent meshComponentFound = actor[ActorInfo.RENDER_COMPONENT] as RenderComponent;
            CollisionComponent collisionComponentFound = actor[ActorInfo.OCCLUSION_COMPONENT] as CollisionComponent;

            if (meshComponentFound != null && collisionComponentFound != null)
            {
                var drawableActor = new DrawableActorProxy(actor, meshComponentFound, collisionComponentFound);
                modelsStoredInQuadTree++;
                this.AddDrawableActor(drawableActor);
                return drawableActor.ID;
            }
            else 
                return -1;
        }

        /// <summary>
        /// Draws all of the actors in this tree to the scene
        /// </summary>
        /// <param name="gameTime">Game timing values</param>
        /// <param name="view">camera look at</param>
        /// <param name="projection">viewport projection</param>
        /// <param name="cameraViewFrustum">camera view frustum</param>
        public void Draw(GameTime gameTime, BoundingFrustum cameraViewFrustum, Matrix view, Matrix projection)
        {
            ContainmentType cameraNodeContainment = cameraViewFrustum.Contains(this.boundingBox);

            // check to see if our camera is intersecting with this node's bounding box
            if (cameraNodeContainment != ContainmentType.Disjoint)
            {
                foreach (var drawable in this.drawableActors)
                    drawable.Mesh.Draw((float)gameTime.ElapsedGameTime.TotalSeconds, view, projection);

                // now we propagate the draw call down to our child nodes
                foreach (OctTreeNode node in children)
                    node.Draw(gameTime, cameraViewFrustum, view, projection);
            }
        }

        /// <summary>
        /// Draws bounding boxes for the octree cull voxels
        /// </summary>
        /// <param name="shader"></param>
        /// <param name="graphicsDevice"></param>
        /// <param name="frustum"></param>
        /// <param name="view"></param>
        /// <param name="projection"></param>
        public void DrawDebug(GraphicsDevice graphicsDevice, BoundingFrustum frustum)
        {
             ContainmentType cameraNodeContainment = frustum.Contains(this.boundingBox);

            // check to see if our camera is intersecting with this node's bounding box
             if (cameraNodeContainment != ContainmentType.Disjoint)
             {
                VertexPositionColor[] verts;
                short[] indices;

                Utility.Helpers.BoundingBoxDebugMesh(this.boundingBox, out verts, out indices, Color.Indigo);
                graphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.LineList, verts, 0, 8, indices, 0, 12);

                foreach (var model in this.drawableActors)
                {
                    if (model.Actor is TerrainPatch)
                        (model.Actor as TerrainPatch).DrawDebug(graphicsDevice);
                }

                foreach (var node in this.children)
                    node.DrawDebug(graphicsDevice, frustum);
             }
        }

        /// <summary>
        /// Gets a list of actors that exist in a voxel that the passed in actor currently occupies
        /// </summary>
        /// <param name="actor"></param>
        public DrawableActorProxy[] QueryForLocalActors(Actor actor)
        {
            if (children.Count <= 0)
            {
                return drawableActors.ToArray();
            }
            else
            {
                Vector3 position = actor.Position;

                if (position.Y > center.Y)
                    if (position.Z < center.Z)
                        if (position.X < center.X)
                            return nodeUFL.QueryForLocalActors(actor);
                        else
                            return nodeUFR.QueryForLocalActors(actor);
                    else
                        if (position.X < center.X)
                            return nodeUBL.QueryForLocalActors(actor);
                        else
                            return nodeUBR.QueryForLocalActors(actor);
                else
                    if (position.Z < center.Z)
                        if (position.X < center.X)
                            return nodeDFL.QueryForLocalActors(actor);
                        else
                            return nodeDFR.QueryForLocalActors(actor);
                    else
                        if (position.X < center.X)
                            return nodeDBL.QueryForLocalActors(actor);
                        else
                            return nodeDBR.QueryForLocalActors(actor);
            }
        }


        #region Private Parts

        /// <summary>
        /// Creates the child nodes for this node
        /// </summary>
        private void CreateChildNodes()
        {
            float sizeOver2 = size / 2.0f;
            float sizeOver4 = size / 4.0f;

            nodeUFR = new OctTreeNode(this, center + new Vector3(sizeOver4, sizeOver4, -sizeOver4), sizeOver2);
            nodeUFL = new OctTreeNode(this, center + new Vector3(-sizeOver4, sizeOver4, -sizeOver4), sizeOver2);
            nodeUBR = new OctTreeNode(this, center + new Vector3(sizeOver4, sizeOver4, sizeOver4), sizeOver2);
            nodeUBL = new OctTreeNode(this, center + new Vector3(-sizeOver4, sizeOver4, sizeOver4), sizeOver2);

            nodeDFR = new OctTreeNode(this, center + new Vector3(sizeOver4, -sizeOver4, -sizeOver4), sizeOver2);
            nodeDFL = new OctTreeNode(this, center + new Vector3(-sizeOver4, -sizeOver4, -sizeOver4), sizeOver2);
            nodeDBR = new OctTreeNode(this, center + new Vector3(sizeOver4, -sizeOver4, sizeOver4), sizeOver2);
            nodeDBL = new OctTreeNode(this, center + new Vector3(-sizeOver4, -sizeOver4, sizeOver4), sizeOver2);

            this.children.Add(nodeUFR);
            this.children.Add(nodeUFL);
            this.children.Add(nodeUBR);
            this.children.Add(nodeUBL);

            this.children.Add(nodeDFR);
            this.children.Add(nodeDFL);
            this.children.Add(nodeDBR);
            this.children.Add(nodeDBL);
        }

        /// <summary>
        /// Adds an actor to our octal tree
        /// </summary>
        private void AddDrawableActor(DrawableActorProxy actor)
        {
            // if this node doesnt have any children
            if (children.Count == 0)
            {
                // we add the actor to the list
                this.drawableActors.Add(actor);

                bool maxObjectsReached = (drawableActors.Count > WorldInfo.Meshes_Per_Voxel);
                bool minSizeNotReached = (size > WorldInfo.Minimum_Voxel_Size);

                // if our constraints have been broken...
                if (maxObjectsReached && minSizeNotReached)
                {
                    // we expand our tree
                    CreateChildNodes();

                    // then we distribute every actor
                    foreach (var CurrentActor in drawableActors)
                        DistributeActor(CurrentActor);

                    // clear our list since all of the actors are now in children nodes
                    drawableActors.Clear();
                }
            }
            else
                DistributeActor(actor);
        }

        /// <summary>
        /// Distributes the drawable actor to one of the nodes
        /// </summary>
        private void DistributeActor(DrawableActorProxy actor)
        {
            Vector3 position = actor.Location;

            if (position.Y > center.Y)
                if (position.Z < center.Z)
                    if (position.X < center.X)
                        nodeUFL.AddDrawableActor(actor);
                    else
                        nodeUFR.AddDrawableActor(actor);
                else
                    if (position.X < center.X)
                        nodeUBL.AddDrawableActor(actor);
                    else
                        nodeUBR.AddDrawableActor(actor);
            else
                if (position.Z < center.Z)
                    if (position.X < center.X)
                        nodeDFL.AddDrawableActor(actor);
                    else
                        nodeDFR.AddDrawableActor(actor);
                else
                    if (position.X < center.X)
                        nodeDBL.AddDrawableActor(actor);
                    else
                        nodeDBR.AddDrawableActor(actor);
        }

        #endregion

    }
}
